package it.unibo.refolding.pico.mfp;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.pico.PlainDirectionData;
import it.unibo.refolding.pico.PlainPicoData;
import it.unibo.refolding.pico.mfp.Dwell.DwellAway;
import it.unibo.refolding.pico.mfp.Dwell.DwellBoth;
import it.unibo.refolding.pico.mfp.Dwell.DwellNone;
import it.unibo.refolding.pico.mfp.Dwell.DwellTowards;
import it.unibo.refolding.pico.transformers.ByteArray2ShortArrayStrategyByte;
import it.unibo.refolding.pico.transformers.ByteArray2ShortArrayStrategyDouble;
import it.unibo.refolding.pico.transformers.ByteArray2ShortArrayStrategyFloat;
import it.unibo.refolding.pico.transformers.ByteArray2ShortArrayStrategyInt;
import it.unibo.refolding.pico.transformers.ByteArray2ShortArrayStrategyShort;
import it.unibo.refolding.pico.transformers.Transformer;
import it.unibo.refolding.pico.transformers.Transformers;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;

import com.google.common.base.Preconditions;

public class MfpDataExtractor {
  // Note: First deflection file is written to disk, then LVDT (checked with
  // "stat").
  
  /*
   * Looks like 20 V is a good limit also for MFP (see btn_SinglePull in
   * Puller.ipf). TODO: Is that 20 V of piezo or deflection?
   */
  private static final Logger logger = Logger.getLogger(MfpDataExtractor.class.getSimpleName());

  private static final double ANY_PIEZO_LIMIT_V = 20;

  public static PlainPicoData parse(byte[] deflectionContent, byte[] lvdtContent) {
    return parse(ByteBuffer.wrap(deflectionContent), ByteBuffer.wrap(lvdtContent));
  }

  public static PlainPicoData parse(Path deflectionPath, Path lvdtPath) {
    try {
      return parseThrowingIo(deflectionPath, lvdtPath);
    } catch (IOException e) {
      throw new MyRuntimeException(e.toString(), e);
    }
  }

  public static PlainPicoData parseThrowingIo(Path deflectionPath, Path lvdtPath)
    throws IOException {
    return parse(Files.readAllBytes(deflectionPath), Files.readAllBytes(lvdtPath));
  }

  private static PlainPicoData parse(ByteBuffer deflectionContent, ByteBuffer lvdtContent) {
    /*
     * Basename is limited to 17 characters.
     * 
     * For Both: 10 s <= DwellTime <= 1000 s 1 p/s <= Dwell Rate <= 30 p/s For
     * Away and Towards: DwellRate enabled iff DwellTime >= 10.
     */
    Wave deflectionWave = Waves.readWave(deflectionContent);
    Wave lvdtWave = Waves.readWave(lvdtContent);
    int byteCount = deflectionWave.waveData.size();
    ByteOrder byteOrder = deflectionWave.byteOrderAndVersion.byteOrder;
    Preconditions.checkArgument(lvdtWave.byteOrderAndVersion.byteOrder().equals(byteOrder));
    /*
     * The version is different between dwell_nonedeflection0000.itx and
     * dwell_noneLVDT0000.itx, so don't compare the version numbers.
     */
    Preconditions.checkArgument(byteCount == lvdtWave.waveData.size());
    String bname = CommonThings.replaceLastOccurenceOrFail(lvdtWave.waveHeader.bname(), "LVDT", "");
    WaveHeader header = lvdtWave.waveHeader.withBname(bname);
    Preconditions.checkArgument(
      header.equalsCommonFields(deflectionWave.waveHeader.withBname(CommonThings.replaceLastOccurenceOrFail(
        deflectionWave.waveHeader.bname(), "deflection", ""))),
      "lvdtWave.waveHeader=%s deflectionWave.waveHeader=%s", lvdtWave.waveHeader,
      deflectionWave.waveHeader);
    Preconditions.checkArgument(deflectionWave.unsignedNotes.equals(lvdtWave.unsignedNotes));
    String notes = deflectionWave.notes();
    Map<String, String> notesMap = parseNotes(notes);
    double startDist = Double.parseDouble(notesMap.get("StartDist"));
    double pullDist = Double.parseDouble(notesMap.get("PullDist"));
    byte pullDistSign = Byte.parseByte(notesMap.get("PullDistSign"));
    if (pullDistSign != 1) {
      logger.log(Level.WARNING, "pullDistSign=%s in %s is not supported.", new Object[] {
      pullDistSign, bname});
      return null;
    }
    double piezoSens = Double.parseDouble(notesMap.get("PiezoSens"));
    Preconditions.checkArgument(
      piezoSens < 0, "piezoSens=%s in %s should be negative.", piezoSens, bname);
    boolean velocitySynch = parseNotesBoolean(notesMap.get("VelocitySynch"));
    double velocity = Double.parseDouble(notesMap.get("Velocity"));
    double approachVelocity = Double.parseDouble(notesMap.get("ApproachVelocity"));
    double retractVelocity = Double.parseDouble(notesMap.get("RetractVelocity"));

    double finalApproachVelocity = velocitySynch ? velocity : approachVelocity;
    double finalRetractVelocity = velocitySynch ? velocity : retractVelocity;

    /*
     * I think scanRate takes into acount only "Velocity", and no waiting time
     * at all.
     */
    byte dwellSetting = Byte.parseByte(notesMap.get("DwellSetting"));
    double dwellTime = Double.parseDouble(notesMap.get("DwellTime"));
    // Capture rate during long dwells.
    double dwellRate = Double.parseDouble(notesMap.get("DwellRate"));
    Dwell dwell = createDwell(dwellSetting, dwellTime, dwellRate);
    /*
     * Variable nod = (DwellSetting!=3)+(DwellSetting==4) if ((DwellTime < 10)
     * || (IsDualPull == 1)) NumPtsPerWave += NumPtsPerSec*DwellTime*nod else
     * NumPtsPerWave += DwellRate*DwellTime*nod endif
     */
    double numPtsPerSec = Double.parseDouble(notesMap.get("NumPtsPerSec"));
    double invOLS = Double.parseDouble(notesMap.get("InvOLS"));
    double springC = Double.parseDouble(notesMap.get("SpringC"));
    boolean dualPull = parseNotesBoolean(notesMap.get("IsDualPull"));
    if (dualPull) {
      logger.log(Level.WARNING, "dualPull=%s in %s is not supported.", new Object[] {
      dualPull, bname});
      return null;
    }
    double approachTime = pullDist / finalApproachVelocity;
    double retractTime = pullDist / finalRetractVelocity;
    // TotalTime = 0.5/ApproachScanRate+0.5/RetractScanRate
    double totalTime = (dwell.totalTimeS() < IgorUtil.TIME_THRESHOLD ? dwell.totalTimeS() : 0)
      + approachTime + retractTime;

    int expectedLength = (int) (totalTime * numPtsPerSec);
    int numBytesPerPoint = IgorUtil.numBytesPerPoint(header.type);
    Preconditions.checkArgument(
      byteCount % numBytesPerPoint == 0, "byteCount=%s numBytesPerPoint=%s", byteCount,
      numBytesPerPoint);
    Preconditions.checkArgument(
      Math.abs(expectedLength - byteCount / numBytesPerPoint) < 2,
      "expectedLength=%s byteCount=%s numBytesPerPoint=%s", new Object[] {
      expectedLength, byteCount, numBytesPerPoint});
    /*
     * Variable nod = (DwellSetting != 0)+(DwellSetting == 3) Variable nop =
     * nod*LocalDwellRate*DwellTime+NumPtsPerSec*TotalTime
     */
    /*
     * The only requirement is that MFP Piezo Voltage <= ANY_PIEZO_LIMIT_V. We put 20 to stay in line with PicoForce, so the .svg images look similar.
     */
    double zLsbToMetresFactor = -piezoSens * Transformers.voltLsb(ANY_PIEZO_LIMIT_V);
    Transformer deflectionStrategy = createTransformer(header.type, byteOrder);
    // We divide first by invOLS to get the voltage, and then map it into [0, 20].
    short[] def = deflectionStrategy.transform(
      deflectionWave.waveData.toByteArray(), invOLS * Transformers.voltLsb(Transformers.DEFLECTION_LIMIT_V));
    Transformer heightStrategy = createTransformer(header.type, byteOrder);
    short[] z = heightStrategy.transform(lvdtWave.waveData.toByteArray(), zLsbToMetresFactor);
    int n = def.length;
    Preconditions.checkState(z.length == n);

    int approachLen = (int) (approachTime * numPtsPerSec);
    int retractLen = (int) (retractTime * numPtsPerSec);
    int dwellLen = n - approachLen - retractLen;
    Preconditions.checkState(dwell.totalTimeS() >= IgorUtil.TIME_THRESHOLD == (Math.max(
      dwell.towardsDelayS(), dwell.awayDelayS()) >= IgorUtil.TIME_THRESHOLD));
    Preconditions.checkState(Math.abs(dwellLen
      - (dwell.totalTimeS() >= IgorUtil.TIME_THRESHOLD ? 0 : dwell.totalTimeS()) * numPtsPerSec) < 3);
    int towardsDwellLen = (int) ((dwell.towardsDelayS() >= IgorUtil.TIME_THRESHOLD ? 0
      : dwell.towardsDelayS()) * numPtsPerSec);
    /*
     * Away has the Away waiting time at the end of the curve.
     */
    PlainDirectionData approach = new PlainDirectionData(
      CommonThings.reversed(def, 0, approachLen), CommonThings.reversed(z, 0, approachLen),
      finalApproachVelocity);
    PlainDirectionData retraction = new PlainDirectionData(
      Arrays.copyOfRange(def, approachLen + towardsDwellLen, approachLen + towardsDwellLen
        + retractLen), Arrays.copyOfRange(z, approachLen + towardsDwellLen, approachLen
        + towardsDwellLen + retractLen), finalRetractVelocity);
    // ScanRateHz does *not* include the waiting time.
    return new PlainPicoData(
      0, springC, -piezoSens, startDist / -piezoSens, zLsbToMetresFactor, Transformers.DEFLECTION_LIMIT_V, invOLS,
      1 / (approachTime + retractTime), approach, retraction, pullDist / -piezoSens,
      dwell.towardsDelayS());
  }

  private static Transformer createTransformer(short type, ByteOrder byteOrder) {
    switch (type) {
    case IgorUtil.NT_FP32:
      return new ByteArray2ShortArrayStrategyFloat(byteOrder);
    case IgorUtil.NT_FP64:
      return new ByteArray2ShortArrayStrategyDouble(byteOrder);
    case IgorUtil.NT_I8:
      return new ByteArray2ShortArrayStrategyByte(true);
    case IgorUtil.NT_I16:
      return new ByteArray2ShortArrayStrategyShort(byteOrder, 0, 1, true);
    case IgorUtil.NT_I32:
      return new ByteArray2ShortArrayStrategyInt(byteOrder, 0, 1, true);
    case IgorUtil.NT_I8 | IgorUtil.NT_UNSIGNED:
      return new ByteArray2ShortArrayStrategyByte(false);
    case IgorUtil.NT_I16 | IgorUtil.NT_UNSIGNED:
      return new ByteArray2ShortArrayStrategyShort(byteOrder, 0, 1, false);
    case IgorUtil.NT_I32 | IgorUtil.NT_UNSIGNED:
      return new ByteArray2ShortArrayStrategyInt(byteOrder, 0, 1, false);
    default:
      if ((type & IgorUtil.NT_CMPLX) != 0) {
        throw new IllegalArgumentException("Complex type " + type + " is unsupported.");
      } else {
        throw new IllegalArgumentException("Unsupported type: " + type);
      }
    }
  }

  private static Dwell createDwell(byte dwellSetting, double dwellTime, double dwellRate) {
    DwellSetting type = DwellSetting.valueOf(dwellSetting);
    switch (type) {
    case AWAY:
      return new DwellAway(dwellTime, dwellRate);
    case BOTH:
      Preconditions.checkArgument(dwellTime >= 10);
      return new DwellBoth(dwellTime, dwellRate);
    case NONE:
      return DwellNone.INSTANCE;
    case TOWARDS:
      return new DwellTowards(dwellTime, dwellRate);
    default:
      throw new IllegalArgumentException("kind of impossible");
    }
  }

  private static boolean parseNotesBoolean(String s) {
    boolean f = "0".equals(s);
    boolean t = "1".equals(s);
    Preconditions.checkArgument(f ^ t, "%s is neither 0 nor 1", s);
    return t;
  }

  static Map<String, String> parseNotes(String notes) {
    String[] split = StringUtils.split(notes, '\r');
    Map<String, String> map = new HashMap<>();
    boolean endOfNoteEncountered = false;
    for (String s : split) {
      Preconditions.checkArgument(!endOfNoteEncountered, "EndOfNote found twice.");
      if (s.equals("EndOfNote")) {
        endOfNoteEncountered = true;
      } else {
        int index = s.indexOf(':');
        Preconditions.checkArgument(index != -1, "%s does not have a colon", s);
        String key = s.substring(0, index);
        String old = map.put(key, s.substring(index + 1));
        Preconditions.checkArgument(old == null, "Repeated key: %s.", key);
      }
    }
    return map;
  }
}
